<?php
/**
 * Created by PhpStorm.
 * User: s.utkin
 * Date: 17.09.14
 * Time: 18:19
 */

namespace Resources;


use Resources\Config\PackagesConfig;
use Resources\Layout\Postprocessing;

/**
 * Менеджер ресурсов.
 *
 * @package Resources
 */
class Manager
{
	/**
	 *    Скрипт подключается в тэге HEAD
	 */
	const POS_HEAD = 1;

	/**
	 * Скрипт подключается сразу после открывающего тэга BODY
	 */
	const POS_BEGIN = 2;

	/**
	 * Скрипт подключается перед закрывающим тэгом BODY
	 */
	const POS_END = 3;

	/**
	 * Скрипт размещается в функции window.onload
	 */
	const POS_LOAD = 4;

	/**
	 * Скрипт размещается в функции jQuery.ready
	 */
	const POS_READY = 5;

	/**
	 * Список подключенных css-файлов
	 *
	 * @var string[]
	 */
	protected $cssFiles;

	/**
	 * Список подключенных js-файлов
	 *
	 * @var string[]
	 */
	protected $jsFiles;

	/**
	 * Именованый список конфигурационных классов пакетов
	 *
	 * @var string[]
	 */
	protected $packagesConfig;
	protected $groupsConfig;

	/**
	 * Флаг, показывающий, опубликован ли текущий пакет
	 *
	 * @var bool
	 */
	protected $currentPackageIsPublished = false;

	/**
	 * Список опубликованных пакетов
	 *
	 * @var array
	 */
	protected $publishedPackages;
	protected $publishedGroups;

	/**
	 * Информация о ресурсах пакетов, подгружаемых через ajax
	 *
	 * @var array
	 */
	protected $dynamicResources;

	/**
	 * Позиция подключения js-файла по умолчанию
	 *
	 * @var int
	 */
	public $defaultJsPosition = Postprocessing::POSITION_JS_FILES_HEAD;

	/**
	 * Конструктор.
	 *                    (Ваш Капитан Очевидность)
	 */
	public function __construct()
	{
		# Получаем список конфигов пакетов
		$this->packagesConfig = PackagesConfig::get();
		$this->groupsConfig = PackagesConfig::$groups;

		$this->publishedPackages = Publisher::getRuntimeConfig();
		$this->publishedGroups = Publisher::getRuntimeConfig(true);
	}

	/**
	 * Возвращает список css-файлов
	 *
	 * @return string[]
	 */
	public function getCssFiles()
	{
		return $this->cssFiles;
	}

	/**
	 * Возвращает список js-файлов
	 *
	 * @return string[]
	 */
	public function getJsFiles()
	{
		return $this->jsFiles;
	}

	/**
	 * Регистрирует js-файл
	 *
	 * @param $srcPath - относительный путь к файлу
	 * @param $packageName - имя пакета
	 * @param null $position - позиция вывода
	 * @param bool $external - признак внешнего ресурса
	 */
	protected function registerJsFile($srcPath, $packageName, $scope = null, $position = null, $external = false, $onlyPublish = false)
	{
		$uri = $external ? $srcPath : File::getUrl($srcPath, $packageName, $scope, false);

		if ($position === null) {
			$position = $this->defaultJsPosition;
		}

		if (!$onlyPublish) {
			$this->jsFiles[$position][$uri] = $uri;
		}

		if ($scope == null) {
			$this->dynamicResources[$packageName]['js'][] = $uri;
		}
	}

	/**
	 * Регистрирует css-файл
	 *
	 * @param $srcPath - относительный путь к файлу
	 * @param $packageName - имя пакета
	 * @param string $media - media атрибут link`а
	 * @param bool $external - признак внешнего ресурса
	 */
	protected function registerCssFile($srcPath, $packageName, $scope = null, $media = '', $external = false, $onlyPublish = false)
	{
		$uri = $external ? $srcPath : File::getUrl($srcPath, $packageName, $scope, false);
		$position = Postprocessing::POSITION_CSS_HEAD;

		if (!$onlyPublish) {
			$this->cssFiles[$position][$uri] = $media;
		}

		if ($scope == null) {
			$this->dynamicResources[$packageName]['css'][] = $uri;
		}
	}

	/**
	 * TODO: Удалить функцию
	 *
	 * Регистрирует пакет
	 *
	 * @param $packageName - имя пакета
	 * @throws \Exception
	 */
	public function registerPackage_old($packageName)
	{
		# Список подключенных пакетов
		static $attachedPackages = [];

		# Если пакет уже был подключен ранее - завершаем работу функции
		if (in_array($packageName, $attachedPackages)) {
			return;
		}

		# Если указанный пакет не найден в конфиге - ничего не делаем
		if (!isset($this->packagesConfig[$packageName])) {
			\Log::error('Resource manager', "Package '$packageName' not found.");
			return;
		}

		# Получаем имя класса с конфигом пакета
		$packageClassName = $this->packagesConfig[$packageName];

		if (!class_exists($packageClassName) || !method_exists($packageClassName, 'get')) {
			\Log::error('Resource manager', "Wrong package config: $packageClassName");
			return;
		}

		# Получаем конфиг пакета
		$packageConfig = $packageClassName::$config;

		# Если у пакета есть зависимости - подключаем их
		if (isset($packageConfig['depends'])) {
			foreach ((array)$packageConfig['depends'] as $dependency) {
				$this->registerPackage($dependency);
			}
		}

		# Если пакет не опубликован
		if (!isset($this->publishedPackages[$packageName])) {
			# Добавляем в очередь уведомление о необходимости опубликовать пакет
			Publisher::publishPackage($packageName);
		}

		# Подключаем js-файлы
		if (isset($packageConfig['js'])) {
			foreach ($packageConfig['js'] as $jsFile) {
				$external = isset($jsFile['external']) && $jsFile['external'];
				$this->registerJsFile($jsFile['src'], $packageName, null, $jsFile['position'], $external);
			}
		}

		# Подключаем css-файлы
		if (isset($packageConfig['css'])) {
			foreach ($packageConfig['css'] as $cssFile) {
				$external = isset($jsFile['external']) && $jsFile['external'];
				$this->registerCssFile($cssFile['src'], $packageName, null, $cssFile['media'], $external);
			}
		}

		# Заносим имя пакета в список подключенных пакетов
		$attachedPackages[] = $packageName;
	}

	/**
	 * Регистрирует пакет
	 *
	 * @param $packageName - имя пакета
	 * @param bool $onlyPublish
	 */
	public function registerPackage($packageName, $onlyPublish = false)
	{
		# Получаем имя класса с конфигом пакета
		$packageClassName = $this->packagesConfig[$packageName];

		# Получаем конфиг пакета
		$packageConfig = $packageClassName::$config;

		# Если пакет не опубликован
		if (!isset($this->publishedPackages[$packageName])) {
			# Добавляем в очередь уведомление о необходимости опубликовать пакет
			Publisher::publishPackage($packageName);
		}

		# Подключаем js-файлы
		if (isset($packageConfig['js'])) {
			foreach ($packageConfig['js'] as $jsFile) {
				$external = isset($jsFile['external']) && $jsFile['external'];
				$this->registerJsFile($jsFile['src'], $packageName, null, $jsFile['position'], $external, $onlyPublish);
			}
		}

		# Подключаем css-файлы
		if (isset($packageConfig['css'])) {
			foreach ($packageConfig['css'] as $cssFile) {
				$external = isset($jsFile['external']) && $jsFile['external'];
				$this->registerCssFile($cssFile['src'], $packageName, null, $cssFile['media'], $external, $onlyPublish);
			}
		}
	}


	public function registerGroup($groupName)
	{
		$positions = Group::getResourcesPositions($groupName);
		$groupPackageName = 'group' . ucfirst($groupName);

		foreach ($positions['js'] as $position) {
			$this->registerJsFile(
				'js/' . $groupName . '_' . $position . '.js',
				$groupPackageName,
				$groupName,
				$position
			);
		}

		foreach ($positions['css'] as $position) {
			$this->registerCssFile(
				'css/' . $groupName . '_' . $position . '.css',
				$groupPackageName,
				$groupName
			);
		}

		if (!isset($this->publishedGroups[$groupName])) {
			$this->publishedGroups[$groupName] = 1;
			Publisher::publishGroup($groupName);
		}
	}


	/**
	 * Возвращает список inline-фрагментов
	 *
	 * @param \Response $response - экземпляр ответа
	 * @param $codeType - тип кода фрагмента ('css' или 'js')
	 * @return array
	 */
	public function getCode(\Response $response, $codeType)
	{
		$addedFragmentsList = [];

		$result = [];
		$codeIndex = 'resourcePackages_' . strtolower($codeType) . 'Code';
		$jsInfo = (array)$response->get($codeIndex);

		foreach ($jsInfo as $jsItem) {
			if (in_array($jsItem['id'], $addedFragmentsList)) {
				continue;
			}

			$result[$jsItem['position']][] = $jsItem['content'];
			$addedFragmentsList[] = $jsItem['id'];
		}

		return $result;
	}

	/**
	 * Возврашает полные пути к файлам по маске отнсительного пути
	 *
	 * @param $relativeMask - маска
	 * @return array
	 * @throws \Exception
	 */
	public static function getFilesByRelativeMask($relativeMask)
	{
		$filesList = [];

		if (strpos($relativeMask, '*') === false) {
			$file = Publisher::getRealFilePath($relativeMask);
			if ($file && is_file($file)) {
				$parts = explode($relativeMask, $file);
				$filesList[] = $relativeMask . $parts[1];
				return $filesList;
			}
		}

		$fileMask = basename($relativeMask);
		$relativeDir = dirname($relativeMask);
		$realDirPath = Publisher::getRealFilePath($relativeDir);
		$path[] = $realDirPath . '/' . $fileMask;

		while(count($path) != 0)
		{
			$currentPath = array_shift($path);

			foreach(glob($currentPath) as $item)
			{
				if (is_dir($item)) {
					$path[] = $item . '/*';
				} elseif (is_file($item)) {
					$parts = explode($relativeDir, $item);
					$filesList[] = $relativeDir . $parts[1];
				}
			}
		}

		return $filesList;
	}

	/**
	 * Возвращает информацию о ресурсах пакетов в формате
	 * [
	 * 	'packageName1' => [
	 * 		'js' => [ 'url1', 'url2', 'url3', ... 'urlN' ],
	 * 		'css' => [ 'url1', 'url2', 'url3', ... 'urlN' ],
	 * 	],
	 * 'packageName2' => [
	 * 		'js' => [ 'url1', 'url2', 'url3', ... 'urlN' ],
	 * 		'css' => [ 'url1', 'url2', 'url3', ... 'urlN' ],
	 * 	],
	 * ...
	 * 'packageName3' => [
	 * 		'js' => [ 'url1', 'url2', 'url3', ... 'urlN' ],
	 * 		'css' => [ 'url1', 'url2', 'url3', ... 'urlN' ],
	 * 	]
	 * ]
	 *
	 * @return array
	 */
	public function getDynamicResources()
	{
		return $this->dynamicResources;
	}

	protected function calculateDependencies(array $packagesList, $clearBuffer = false)
	{
		static $packagesListWithDependencies = [];

		if ($clearBuffer) {
			$packagesListWithDependencies = [];
		}

		foreach ($packagesList as $package) {
			if (in_array($package, $packagesListWithDependencies)) {
				continue;
			}

			# Если указанный пакет не найден в конфиге - ничего не делаем
			if (!isset($this->packagesConfig[$package])) {
				\Log::error('Resource manager', "Package '$package' not found.");
				continue;
			}

			# Получаем имя класса с конфигом пакета
			$packageClassName = $this->packagesConfig[$package];

			if (!class_exists($packageClassName) || !method_exists($packageClassName, 'get')) {
				\Log::error('Resource manager', "Wrong package config: $packageClassName");
				continue;
			}

			# Получаем конфиг пакета
			$packageConfig = $packageClassName::$config;

			# Если у пакета есть зависимости - добавляем их к списку
			if (isset($packageConfig['depends'])) {
				foreach ((array)$packageConfig['depends'] as $dependency) {
					$this->calculateDependencies([$dependency]);
				}
			}

			if (in_array($package, $packagesListWithDependencies)) {
				continue;
			}

			$packagesListWithDependencies[] = $package;
		}

		return $packagesListWithDependencies;
	}

	/**
	 * @param array $packagesList
	 * @return array
	 */
	public function calculateGroups(array $packagesList)
	{
		$packagesList = $this->calculateDependencies($packagesList);
		$allPackages = $packagesList;

		$groups = [];
		$alonePackages = [];

		$groupsInfo = [];
		foreach ($this->groupsConfig as $groupName => $packages) {
			$groupsInfo[$groupName]['count'] = count($packages);
			$hits = 0;
			foreach ($packagesList as $package) {
				if (in_array($package, $packages)) {
					$hits++;
				}
			}
			$groupsInfo[$groupName]['hits'] = $hits;
		}

		uasort($groupsInfo, function($a, $b)
			{
				if ($a['hits'] > $b['hits']) {return -1;}
				if ($a['hits'] < $b['hits']) {return 1;}
				if ($a['count'] > $b['count']) {return  1;}
				if ($a['count'] < $b['count']) {return  -1;}

				return 0;
			}
		);

		$groupNames = array_keys($groupsInfo);

		while (count($packagesList)) {
			$package = array_shift($packagesList);
			$groupsRemoveList = [];

			$selectedGroup = '';
			foreach ($groupNames as $groupName) {
				$key = array_search($package, $this->groupsConfig[$groupName]);
				if ($key !== false) {
					$selectedGroup = $groupName;
					$groups[] = $groupName;
					$groupsRemoveList[] = $groupName;
					break;
				}
			}

			if (!empty($selectedGroup)) {
				$packagesRemoveList = $this->groupsConfig[$selectedGroup];

				foreach ($groupNames as $index => $grp) {
					$intersect = array_intersect($packagesRemoveList, $this->groupsConfig[$grp]);
					if (!empty($intersect)) {
						$groupsRemoveList[] = $grp;
					}
				}

				# удалить все группы, где есть пакеты из найденной группы
				$groupNames = array_diff($groupNames, $groupsRemoveList);
				$packagesList = array_diff($packagesList, $packagesRemoveList);

			} else {
				$alonePackages[] = $package;
			}
		}

		$result = [
			'groups' => (array)$groups,
			'alonePackages' => (array)$alonePackages,
			'otherPackages' => (array)array_diff($allPackages, $alonePackages)
		];

		return $result;
	}
} 